SpotInstanceとJMeterを使って400万req/minの負荷試験を行う
Apache JMeterのMaster/Slave構成
シナリオを用いた負荷試験といえばJMeterということで、使ったことがある方も多いかと思います。しかし、ほとんどの方は自分のPCを使ってやっている程度ではないでしょうか。最近は、スマホ連動のシステムが多くなってきていますので、1台のPCから負荷を掛けたとしても大した負荷試験になりません。そこで、今回はJMeterをMaster/Slaveのクラスター構成にしてドカーンと同時アクセスを行いたいと思います。
クラスメソッドの負荷試験の歴史
創業時から業務系のシステム開発が多かったことから、レスポンスは3秒以内でOKとか、ピーク時の同時ユーザは100名といった、緩い条件をクリアすれば良かったことが懐かしく思います。今は、ユーザ数・データ量・トランザクション数・トラフィック等が爆発的に増える可能性のあるプロジェクトも多く、負荷試験は必須項目になりつつあります。また、UXを語る上でアプリのレスポンスタイムは非常に重要であり、どんなに機能が豊富でもサクサク感が無ければユーザが離れてしまいます。通販サイトにおいてはレスポンスタイムが売上げに直結するほどです。以下は、実際のプロジェクトで求められている当社のパフォーマンスの歴史をざっくり表したものです。最近のパフォーマンス要件は未知の領域に来ています。
- 2010年 : サーバ設計って何?
- 2011年 : 100万req/年のサーバ設計
- 2012年 : 100万req/月のサーバ設計
- 2013年 3月:100万req/日のサーバ設計
- 2013年 4月:100万req/時のサーバ設計
- 2013年 5月:100万req/分のサーバ設計(今ココ)
- 2013年 6月:100万req/秒のサーバ設計(予想)
AWSを用いて負荷試験を簡単に行う
私はAWS中毒のため、AWS環境を用いて負荷試験を行います。まずは、負荷を掛けられる受け側のサーバのセットアップをしたいと思います。今回は直接サーバには向けずにロードバランサーを介して負荷を掛けることで、後にCloudWatchによる統計情報を見て動作を確認したいと思います。
受け側のサーバ構築
まずは受け側のサーバを構築します。お安くテストするためにSpotInstanceを指定して作成します。SpotInstanceは指値で空いているサーバを安く利用する仕組みです。いつ落ちるか分かりませんが、お試しにはぴったりです。インスタンスが消えるのが怖い方はAMIを作成しておけば後でまた立ち上げ直すことができます。
続けてWebサーバを立ててみます。今回は簡単な動作確認だけなので、index.phpを見るとPHP情報が表示されるようにします。
> ssh -i key_name.pem ec2-user@セットアップするサーバ名 $ sudo yum update -y $ sudo yum install httpd-devel php-devel $ sudo chkconfig httpd on $ sudo service httpd start $ sudo vim /var/www/html/index.php <?php phpinfo(); ?>
画面にはこんな感じで表示されればOKです。
続いて、セットアップ済みのサーバを起動したり、オートスケールさせたりできるようにAMIの作成をしておきます。
ロードバランサーの作成とサーバの登録
負荷を掛けるための入り口となるロードバランサーの作成を行います。ヘルスチェックのURLはアクセス可能なものにしてください。
無事に登録が完了するとステータスがIn Serviceになります。
負荷側のサーバを構築する
負荷側のサーバはクラスター構成になりますので、まずはじめに共通のセキュリティグループから作成します。1099番ポートはJMeterの待ち受け用で、30000-60000番ポートはJMeterがサーバ間でRMI通信(Remote Method Invocation:Javaでリモートオブジェクトをコールする仕組み)をするために使う作業用ポートです。アクセス制限のソースの指定に自分自身のセキュリティグループを指定しているのがポイントですね。22番ポートはSSHアクセス用にグローバルなIPアドレスを指定しました。
受け側サーバと同じように負荷側のサーバもSpotInstanceで作成します。そして、SSHからアクセスしてJMeterのセットアップを行います。サーバ起動時にJMeterがサーバモードで自動起動するようにしておきます。デフォルトで1099番ポートを指定しています。
$ sudo yum update -y $ wget http://ftp.riken.jp/net/apache//jmeter/binaries/apache-jmeter-2.9.tgz $ tar zxvf apache-jmeter-2.9.tgz $ sudo vim /etc/rc.local /home/ec2-user/apache-jmeter-2.9/bin/jmeter-server &
JMeterを起動後にプロセス表示が以下のようになっていればOKです。
$ ps -ax ... 19717 pts/0 S 0:00 /bin/sh ./jmeter-server 19719 pts/0 S 0:00 /bin/sh ./jmeter -Dserver_port=1099 -s -j jmeter-server.log 19721 pts/0 Sl 0:00 java -server -XX:+HeapDumpOnOutOfMemoryError -Xms512m -Xmx512m ... ...
セットアップ済みのJMeterサーバは、汎用的に使えるものとしてAMIを作成しておきます。そして、作業用のサーバを11台(Master1台+Slave10台)を起動します。SpotInstanceなら怖くないっw。m3.xlargeいってまえー
SpotInstance手続き中。。。
すばらしい光景です
JMeterのMasterサーバをセットアップする
起動した各サーバは、JMeter SlaveとしてMasterからの接続待ち状態になっています。そこで、1台をMasterサーバとして動作するようにセットアップします。ラベル付けしましたのでSSHログインして設定します。JMeterでMasterとして動作させるためには、jmeter.propertiesにSlaveとして動作するリモートサーバ名を列挙する必要があります。
$ cd /home/ec2-user/apache-jmeter-2.9/bin/ $ vim jmeter.properties
remote_hostsの部分を編集してリモートサーバを列挙します。ここで注意点としては、IPやEIPを指定しないでください。JMeterがRMI通信をするときにエラーで落ちてしまいます。解決策はあるのですが設定を最小限にするために今回は省きました。
remote_hosts=ec2-AAA-AAA-AAA-AAA.compute-1.amazonaws.com:1099,ec2-BBB-BBB-BBB-BBB.compute-1.amazonaws.com:1099,ec2-CCC-CCC-CCC-CCC.compute-1.amazonaws.com:1099,......
設定が完了しましたら保存して閉じます。デスクトップPCでは通常JMeterをGUIモードで起動しますが、サーバ内ではnon-GUIモードで起動させます。また、リモートサーバを利用する指定をすると、Master/Slave構成で起動してくれます。で、起動する前にシナリオを指定するのを忘れていましたので、JMeterのシナリオを作成してアップロードしましょう。
JMeterのシナリオを作成してアップロードする
デスクトップPCでJMeterのシナリオを作成して、JMeterのMasterサーバにアップロードします。
スレッドの作成をします。スレッド数100で無限ループとしました。
HTTPリクエストの作成
負荷の受け側であるELBアドレスを指定
結果をテキストで出力する設定
シナリオが完成しましたので保存します。そして、scpコマンドでJMeterのMasterサーバにアップロードします。
> $ scp -i key_name.pem jmeter-senario1.jmx ec2-user@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com: jmeter-senario1.jmx 100% 6283 6.1KB/s 00:00
負荷試験を開始してCloudWatchで確認する
全ての準備が整いましたので負荷試験を開始します。JMeterのMasterサーバにログインして以下のコマンドを打ってください。
$ /home/ec2-user/apache-jmeter-2.9/bin/jmeter -n -t jmeter-senario1.jmx -r Created the tree successfully using jmeter-senario1.jmx Configuring remote engine for ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:1099 Using remote object: UnicastRef [liveRef: [endpoint:[10.149.12.35:44724](remote),objID:[43e0835:13e79186db6:-7fff, -7005235020197458357]]] Configuring remote engine for ec2-yyy-yyy-yyy-yyy.compute-1.amazonaws.com:1099 Using remote object: UnicastRef [liveRef: [endpoint:[10.149.12.190:46719](remote),objID:[27311e4:13e79185206:-7fff, 549179843596194467]]] ... Starting remote engines Starting the test @ Mon May 06 09:52:35 UTC 2013 (1367833955389) Waiting for possible shutdown message on port 4445
Waiting forというところまで出たら起動に成功しています!もし、エラーで落ちてしまった場合には、対象サーバを再起動してみてください。私は以下のようなエラーが何回か出ました。また、jmeterを既に起動している場合には落としてください。
Error in NonGUIDriver java.lang.IllegalArgumentException: The following remote engines could not be configured:[ec2-zzz-zzz-zzz-zzz.compute-1.amazonaws.com:1099]
Error in NonGUIDriver java.lang.IllegalStateException: Engine is busy - please try later
または以下のコマンドが有効かもしれません。
$ /home/ec2-user/apache-jmeter-2.9/bin/shutdown
CloudWatchでCPU負荷とリクエスト数を確認する
実際にどんな負荷が掛かっているかCloudWatchを使って受け側のサーバ状況を確認します。今回は受け側を1台構成でm1.smallインスタンスにしました。結果、CPU負荷が100%でレスポンスの遅延が発生してしまっています。
m1.smallのCPU使用率:100%張り付いています。。。
m1.smallの遅延:遅延が大きいですね。。。
m1.smallのリクエスト対応数
サーバをパワーアップして負荷試験する
m1.smallでは弱すぎましたね。AWSの中の人から怒られてしまいそうです。そこで、m3.xlargeにして試してみましょう。起動したインスタンスはDetailedMonitoringにしておきましょう。起動したサーバはELBに追加登録します。登録完了後、m1.smallのインスタンスはELBから外します。
m3.xlargeのCPU使用率:だいぶ下がりました!
m3.xlargeの遅延:遅延も下がってサクサク動きます!
m3.xlargeのリクエスト対応数:対応できるリクエスト数も10倍程度にドーンと伸びています!
m3.xlargeの本気を見せてもらおう
それでは、負荷を掛ける側のサーバをどんどん増やしてみましょう。インスタンスを立てまくります。
そして負荷を掛けました!、、、、しかし、なぜか思ったようにリクエスト数が伸びません。
負荷試験の際のチューニングポイント
負荷側のサーバ台数をいくら増やしても直に結果に繋がりませんでした。そこで、あれこれと試す中で出てきた答えが以下の通りです。
- 受け側のサーバのCPU使用率を低くするためにサーバ台数を増やす。今回は1台から9台にしました。
- 受け側からレスポンスするデータサイズをできるだけ小さくする。今回は50KBから1KBにしました。
- JMeterのスレッドグループを増やす。スレッドを増やすよりもスレッドグループを増やしたほうが良さそうです。
これらの施策の結果、以下のようになりました!
チューニング後のCPU使用率
チューニング後の遅延
チューニング後のリクエスト対応数:脅威の400万req/min越え
受け側のサーバに負荷が掛かっていますので、安定稼働をさせるためには台数をもっと増やす必要がありそうですね。また、負荷側のチューニングをさらに行うことでもっと効率良く大量のリクエストを飛ばすことができる気がしています。今回はとりあえずここまでとします。
まとめ
今回は、受け側のサーバでPHPを実行する簡単な構成でしたので、データベースアクセスやディスクアクセスの無い、ボトルネックがほとんど無い中でテストを行いました。実際のシステムでは、データの圧縮、キャッシュ、CloudFrontを使ったコンテンツ配信、Expiresヘッダを付けたリクエスト数の削減等々、様々な知恵を絞って負荷に備えます。それにしても、ELBの性能は凄まじいですね。負荷が上がった場合、ロードバランサーはボトルネックになったり、単一障害点になるかなと思ったのですが、安定して動作していました。世の中のほとんどのWebシステムはELB経由で良いのではと思いました。また、今回はSpotInstanceを上限いっぱいまで確保して数時間使いましたが、米国バージニア価格ですと、m3.xlargeのSpotInstanceは、m1.smallのオンデマンド価格の3割引程度でしたので、かなりお安く負荷試験を行うことができました。当初は100万req/minを目指していたのですが、最終的には400万req/minまで上げることができました。これは、66000req/secになります。1時間あたり2億4千万req、1日あたり57億6千万reqです!?。もし、このようなアクセスの下でトランザクションを扱うのであれば、DynamoDBを使ってみてはいかがでしょうか。クラスター化されたサーバ群のセッション管理であればElastiCacheでも良いですね。ということで、ますますAWSにどっぷりな感じになりそうです。